package io.gitee.dtdage.app.boot.starter.pay.wx.service;

import io.gitee.dtdage.app.boot.starter.common.utils.function.Function;
import io.gitee.dtdage.app.boot.starter.pay.common.context.ExtractBean;
import io.gitee.dtdage.app.boot.starter.pay.common.context.RefundBean;
import io.gitee.dtdage.app.boot.starter.pay.wx.context.ConfigBean;
import org.springframework.boot.configurationprocessor.json.JSONArray;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.Signature;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

/**
 * 微信交易接口
 *
 * @author WFT
 * @since 2024/4/14
 */
public abstract class BaseTradeService extends io.gitee.dtdage.app.boot.starter.pay.common.service.BaseTradeService<ConfigBean> implements ApplicationContextAware {

    private ConfigureService<?> configService;

    @Override
    protected ConfigBean getConfigure(String clientId) {
        return this.configService.getConfigure(clientId);
    }

    @Override
    public void setApplicationContext(ApplicationContext context) {
        this.configService = context.getBean(ConfigureService.class);
    }

    /**
     * @noinspection SameParameterValue, SpellCheckingInspection
     */
    protected <R> R execute(String uri, String body, ConfigBean configure, HttpMethod method, Function<String, R> callback) throws Exception {
        //  设置请求头
        MultiValueMap<String, String> headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        headers.add("Accept", String.join(",", "application/json", "text/plain", "*/*"));
        headers.add("Authorization", "WECHATPAY2-SHA256-RSA2048 " + this.getToken(uri, body, configure, method));
        headers.add("Wechatpay-Serial", configure.getSerialNumber());
        //  拼接接口地址
        String api = "https://api.mch.weixin.qq.com" + uri;
        //  发送请求,并调用回调接口
        return callback.apply(new RestTemplate().exchange(api, method, new HttpEntity<>(body, headers), String.class).getBody());
    }

    /**
     * @noinspection SpellCheckingInspection
     */
    @Override
    public <T extends ExtractBean> void extract(T param) throws Exception {
        //  获取客户端配置
        ConfigBean configure = this.getConfigure(param.getClientId());
        //  转账明细
        JSONObject detail = new JSONObject()
                .put("out_detail_no", String.valueOf(param.getId()))
                .put("transfer_amount", param.getExtractPrice())
                .put("transfer_remark", param.getExtractTitle())
                .put("openid", param.getOutPayeeId())
                .put("user_name", param.getOutPayeeName());
        //  请求体
        JSONObject data = new JSONObject()
                .put("appid", configure.getAppId())
                .put("out_batch_no", String.valueOf(param.getId()))
                .put("batch_name", param.getExtractTitle())
                .put("batch_remark", param.getExtractTitle())
                .put("total_amount", param.getExtractPrice())
                .put("total_num", 1)
                .put("transfer_detail_list", new JSONArray().put(detail))
                .put("transfer_scene_id", configure.getExtSceneId())
                .put("notify_url", String.format(configure.getExtNotifyPath(), param.getId()));
        //  调用接口
        this.execute("/v3/transfer/batches", data.toString(), configure, HttpMethod.POST, response -> 0);
    }

    @Override
    public <T extends RefundBean> void refund(T param) throws Exception {
        //  获取客户端配置
        ConfigBean configure = this.getConfigure(param.getClientId());
        //  退款明细
        JSONObject detail = new JSONObject()
                .put("refund", param.getRefundPrice())
                .put("total", param.getTradePrice())
                .put("currency", "CNY");
        //  请求体
        JSONObject data = new JSONObject()
                .put("out_trade_no", param.getTradeNo())
                .put("out_refund_no", param.getId())
                .put("notify_url", String.format(configure.getRefNotifyPath(), param.getId()))
                .put("amount", detail);
        //  调用接口
        this.execute("/v3/refund/domestic/refunds", data.toString(), configure, HttpMethod.POST, response -> 0);
    }

    protected Map<String, Object> callback(ConfigBean configure, String response) throws Exception {
        //  生成时间戳和随机字符串
        String timestamp = this.getTimestamp();
        String nonceStr = this.getNonceStr();
        //  获得预付款编号,拼接请求体
        String prepayId = new JSONObject(response).getString("prepay_id");
        String body = "prepay_id=" + prepayId;
        //  构建签名串
        byte[] bytes = this.toMessageBytes(configure.getAppId(), timestamp, nonceStr, body);
        //  构建返回值
        Map<String, Object> map = new HashMap<>(6);
        //  应用编号
        map.put("appId", configure.getAppId());
        //  商户编号
        map.put("merchantId", configure.getMchId());
        //  时间戳
        map.put("timestamp", timestamp);
        //  随机字符串
        map.put("nonceStr", nonceStr);
        //  预支付交易会话标识,用于后续接口调用中使用，该值有效期为2小时.
        map.put("package", body);
        //  签名
        map.put("signature", this.signature(bytes, configure));
        return map;
    }

    private String getTimestamp() {
        return String.valueOf(System.currentTimeMillis() / 1000);
    }

    private String getNonceStr() {
        return String.valueOf(System.nanoTime());
    }

    /**
     * @noinspection SpellCheckingInspection
     */
    private String getToken(String uri, String body, ConfigBean configure, HttpMethod method) throws Exception {
        //  生成随机字符串和时间戳
        String str = this.getNonceStr();
        String timestamp = this.getTimestamp();
        //  签名
        String signature = this.signature(this.toMessageBytes(method.toString(), uri, timestamp, str, body), configure);
        //  生成Token
        return "mchid=\"" + configure.getMchId() + "\","
                + "nonce_str=\"" + str + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + configure.getSerialNumber() + "\","
                + "signature=\"" + signature + "\"";
    }

    private byte[] toMessageBytes(String... args) {
        StringBuilder builder = new StringBuilder();
        for (String item : args) {
            if (null == item) {
                builder.append("\n");
                continue;
            }
            builder.append(item).append("\n");
        }
        return builder.toString().getBytes(StandardCharsets.UTF_8);
    }

    private String signature(byte[] data, ConfigBean configure) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        KeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(configure.getPrivateKey()));
        signature.initSign(KeyFactory.getInstance("RSA").generatePrivate(spec));
        signature.update(data);
        return Base64.getEncoder().encodeToString(signature.sign());
    }

}
